/*
 * Copyright (C) 2008-2010 Martin Riesz <riesz.martin at gmail.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.pneditor.editor.commands;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.pneditor.petrinet.Arc;
import org.pneditor.petrinet.ArcEdge;
import org.pneditor.petrinet.Element;
import org.pneditor.petrinet.ElementCloner;
import org.pneditor.petrinet.PetriNet;
import org.pneditor.petrinet.Place;
import org.pneditor.petrinet.PlaceNode;
import org.pneditor.petrinet.ReferencePlace;
import org.pneditor.petrinet.Subnet;
import org.pneditor.petrinet.Transition;
import org.pneditor.util.CollectionTools;
import org.pneditor.util.Command;

/**
 * 
 * @author Martin Riesz <riesz.martin at gmail.com>
 */
public class ReplaceSubnetCommand implements Command
{

	private Subnet subnet;
	private Subnet storedSubnet;
	private List<Element> previousElements;
	private List<Element> newElements = new LinkedList<Element>();
	private Set<Command> deleteReferenceArcCommands = new HashSet<Command>();
	private PetriNet petriNet;

	public ReplaceSubnetCommand(Subnet subnet, Subnet storedSubnet,
			PetriNet petriNet)
	{
		this.petriNet = petriNet;
		this.subnet = subnet;
		Subnet clonedSubnet = ElementCloner.getClone(storedSubnet, petriNet);
		petriNet.getNodeLabelGenerator()
				.setLabelsToReplacedSubnet(clonedSubnet);
		this.storedSubnet = clonedSubnet;

	}

	private Set<ReferencePlace> getReferencePlaces(Subnet subnet)
	{
		Set<ReferencePlace> referencePlaces = new HashSet<ReferencePlace>();
		for (Element element : subnet.getElements())
		{
			if (element instanceof ReferencePlace)
			{
				ReferencePlace referencePlace = (ReferencePlace) element;
				referencePlaces.add(referencePlace);
			}
		}
		return referencePlaces;
	}

	private Set<ReferencePlace> getWeakEquivalents(
			ReferencePlace referencePlace, Set<ReferencePlace> referencePlaces)
	{
		Set<ReferencePlace> weakEquivalents = new HashSet<ReferencePlace>();
		for (ReferencePlace anotherReferencePlace : referencePlaces)
		{
			if (referencePlace != anotherReferencePlace)
			{
				if (weakEquivalentReferencePlaces(referencePlace,
						anotherReferencePlace))
				{
					weakEquivalents.add(anotherReferencePlace);
				}
			}
		}
		return weakEquivalents;
	}

	private boolean isWeakEquivalentWithAnotherFrom(
			ReferencePlace referencePlace, Set<ReferencePlace> referencePlaces)
	{
		return !getWeakEquivalents(referencePlace, referencePlaces).isEmpty();
	}

	private boolean isStrongEquivalentWithAllWeakEquivalents(
			ReferencePlace referencePlace, Set<ReferencePlace> referencePlaces)
	{
		Set<ReferencePlace> weakEquivalents = getWeakEquivalents(
				referencePlace, referencePlaces);
		for (ReferencePlace weakEquivalent : weakEquivalents)
		{
			if (!strongEquivalentReferencePlaces(referencePlace, weakEquivalent))
			{
				return false;
			}
		}
		return true;
	}

	private boolean areAllStrongEquivalent(Set<ReferencePlace> referencePlaces)
	{
		ReferencePlace someEquivalent = CollectionTools
				.getFirstElement(referencePlaces);
		for (ReferencePlace referencePlace : referencePlaces)
		{
			if (!strongEquivalentReferencePlaces(referencePlace, someEquivalent))
			{
				return false;
			}
		}
		return true;
	}

	private void connect(ReferencePlace storedReferencePlace,
			ReferencePlace previousReferencePlace, Set<ReferencePlace> resolved)
	{
		storedReferencePlace.setConnectedPlace(previousReferencePlace
				.getConnectedPlaceNode());
		resolved.add(storedReferencePlace);
		resolved.add(previousReferencePlace);
	}

	private ReferencePlace getNewReferencePlace(PlaceNode referencedPlaceNode,
			ReferencePlace model)
	{
		ReferencePlace newReferencePlace = new ReferencePlace(
				referencedPlaceNode);
		petriNet.getNodeSimpleIdGenerator().setUniqueId(newReferencePlace);
		newReferencePlace.setCenter(
				referencedPlaceNode.getCenter().x - subnet.getCenter().x,
				referencedPlaceNode.getCenter().y - subnet.getCenter().y);
		for (Arc arc : model.getConnectedArcs())
		{ // TODO: !!! what if there are ReferenceArcs?
			Arc newArc = new Arc(newReferencePlace, arc.getTransition(),
					arc.isPlaceToTransition());
			newElements.add(newArc);
		}
		newElements.add(newReferencePlace);
		return newReferencePlace;
	}

	public void execute()
	{
		Set<ReferencePlace> previousReferencePlaces = getReferencePlaces(subnet);
		Set<ReferencePlace> storedReferencePlaces = getReferencePlaces(storedSubnet);
		Set<ReferencePlace> resolved = new HashSet<ReferencePlace>();

		for (ReferencePlace storedReferencePlace : storedReferencePlaces)
		{
			if (!isWeakEquivalentWithAnotherFrom(storedReferencePlace,
					storedReferencePlaces))
			{
				Set<ReferencePlace> weakEquivalentsFromPrevious = getWeakEquivalents(
						storedReferencePlace, previousReferencePlaces);
				if (weakEquivalentsFromPrevious.size() == 1)
				{
					ReferencePlace previousWeakEquivalent = CollectionTools
							.getFirstElement(getWeakEquivalents(
									storedReferencePlace,
									previousReferencePlaces));
					connect(storedReferencePlace, previousWeakEquivalent,
							resolved);
				}
			}
		}

		for (ReferencePlace storedReferencePlace : storedReferencePlaces)
		{
			if (!resolved.contains(storedReferencePlace))
			{
				Set<ReferencePlace> storedEquivalents = getWeakEquivalents(
						storedReferencePlace, storedReferencePlaces);
				storedEquivalents.add(storedReferencePlace);

				Set<ReferencePlace> previousEquivalents = getWeakEquivalents(
						storedReferencePlace, previousReferencePlaces);

				if (areAllStrongEquivalent(storedEquivalents)
						&& areAllStrongEquivalent(previousEquivalents))
				{

					for (ReferencePlace previousEquivalent : previousEquivalents)
					{
						if (!resolved.contains(previousEquivalent))
						{
							ReferencePlace storedEquivalent = CollectionTools
									.getFirstElementNotIn(storedEquivalents,
											resolved);

							if (storedEquivalent == null)
							{
								ReferencePlace someStoredEquivalent = CollectionTools
										.getFirstElement(storedEquivalents);
								storedEquivalent = getNewReferencePlace(
										previousEquivalent
												.getConnectedPlaceNode(),
										someStoredEquivalent);
							}
							connect(storedEquivalent, previousEquivalent,
									resolved);
						}
					}
				}
			}
		}

		Set<ReferencePlace> unresolvedStored = CollectionTools
				.getElementsNotIn(storedReferencePlaces, resolved);
		Set<ReferencePlace> unresolvedPrevious = CollectionTools
				.getElementsNotIn(previousReferencePlaces, resolved);
		if (areAllStrongEquivalent(unresolvedStored)
				&& areAllStrongEquivalent(unresolvedPrevious)
				&& unresolvedStored.size() >= 1
				&& unresolvedPrevious.size() >= 1)
		{
			for (ReferencePlace unresolvedOnePrevious : unresolvedPrevious)
			{
				ReferencePlace unresolvedOneStored = CollectionTools
						.getFirstElementNotIn(unresolvedStored, resolved);

				if (unresolvedOneStored == null)
				{
					ReferencePlace someUnresolvedStored = CollectionTools
							.getFirstElement(unresolvedStored);
					unresolvedOneStored = getNewReferencePlace(
							unresolvedOnePrevious.getConnectedPlaceNode(),
							someUnresolvedStored);
				}
				connect(unresolvedOneStored, unresolvedOnePrevious, resolved);
			}
		}

		for (ReferencePlace previousReferencePlace : previousReferencePlaces)
		{
			if (!resolved.contains(previousReferencePlace))
			{
				deleteReferenceArcCommands.add(new DeleteReferenceArcCommand(
						previousReferencePlace.getReferenceArc()));
			}
		}
		for (Element element : storedSubnet.getElements())
		{
			if (element instanceof ReferencePlace)
			{
				ReferencePlace referencePlace = (ReferencePlace) element;
				if (resolved.contains(referencePlace))
				{
					newElements.add(referencePlace);
				}
			} else
			{
				newElements.add(element);
			}
		}
		for (ReferencePlace storedReferencePlace : storedReferencePlaces)
		{
			if (!resolved.contains(storedReferencePlace))
			{
				Place place = new Place();
				petriNet.getNodeSimpleIdGenerator().setUniqueId(place);
				place.setCenter(storedReferencePlace.getCenter().x,
						storedReferencePlace.getCenter().y);
				for (ArcEdge arc : storedReferencePlace.getConnectedArcEdges())
				{
					arc.setPlaceNode(place);
				}
				place.setLabel("?!");
				newElements.add(place);
			}
		}
		for (Element element : storedSubnet.getElements())
		{ // TODO: clone
			element.setParentSubnet(subnet);
		}
		previousElements = subnet.getElementsCopy();
		subnet.setElements(newElements);
		for (Command deleteReferenceArcCommand : deleteReferenceArcCommands)
		{
			deleteReferenceArcCommand.execute();
		}
	}

	public void undo()
	{
		subnet.setElements(previousElements);
		for (Command deleteReferenceArcCommand : deleteReferenceArcCommands)
		{
			deleteReferenceArcCommand.undo();
		}
	}

	public void redo()
	{
		subnet.setElements(newElements);
		for (Command deleteReferenceArcCommand : deleteReferenceArcCommands)
		{
			deleteReferenceArcCommand.redo();
		}
	}

	private boolean weakEquivalentReferencePlaces(
			ReferencePlace referencePlace1, ReferencePlace referencePlace2)
	{
		Set<Transition> resolved = new HashSet<Transition>();
		for (Transition transition1 : referencePlace1
				.getConnectedTransitionsRecursively())
		{
			for (Transition transition2 : referencePlace2
					.getConnectedTransitionsRecursively())
			{
				if (!resolved.contains(transition1)
						&& !resolved.contains(transition2))
				{
					if (equivalentPlaceTransitionRelation(referencePlace1,
							transition1, referencePlace2, transition2))
					{
						resolved.add(transition1);
						resolved.add(transition2);
					}
				}
			}
		}
		if (resolved.containsAll(referencePlace1
				.getConnectedTransitionsRecursively())
				&& resolved.containsAll(referencePlace2
						.getConnectedTransitionsRecursively()))
		{
			return true;
		}
		return false;
	}

	private boolean strongEquivalentReferencePlaces(
			ReferencePlace referencePlace1, ReferencePlace referencePlace2)
	{
		return weakEquivalentReferencePlaces(referencePlace1, referencePlace2)
				&& referencePlace1.getConnectedTransitionNodes().containsAll(
						referencePlace2.getConnectedTransitionNodes());
	}

	private boolean equivalentPlaceTransitionRelation(PlaceNode placeNode1,
			Transition transition1, PlaceNode placeNode2, Transition transition2)
	{
		Arc arc1pTt = placeNode1.getConnectedArc(transition1, true);
		Arc arc1tTp = placeNode1.getConnectedArc(transition1, false);
		Arc arc2pTt = placeNode2.getConnectedArc(transition2, true);
		Arc arc2tTp = placeNode2.getConnectedArc(transition2, false);
		return equivalentArcs(arc1pTt, arc2pTt)
				&& equivalentArcs(arc1tTp, arc2tTp);
	}

	private boolean equivalentArcs(Arc arc1, Arc arc2)
	{
		if (arc1 == null && arc2 == null)
		{
			return true;
		} else if (arc1 == null || arc2 == null)
		{
			return false;
		} else if (arc1.getMultiplicity() == arc2.getMultiplicity())
		{
			return true;
		}
		return false;
	}

	@Override
	public String toString()
	{
		return "Replace subnet";
	}

}
